﻿using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using WebApiJwtAuthDemo.Models;
using WebApiJwtAuthDemo.Options;

namespace WebApiJwtAuthDemo.Controllers
{
  [Route("api/[controller]")]
  public class JwtController : Controller
  {
    private readonly JwtIssuerOptions _jwtOptions;
    private readonly ILogger _logger;
    private readonly JsonSerializerSettings _serializerSettings;

    public JwtController(IOptions<JwtIssuerOptions> jwtOptions, ILoggerFactory loggerFactory)
    {
      _jwtOptions = jwtOptions.Value;
      ThrowIfInvalidOptions(_jwtOptions);

      _logger = loggerFactory.CreateLogger<JwtController>();

      _serializerSettings = new JsonSerializerSettings
      {
        Formatting = Formatting.Indented
      };
    }

    [HttpPost("login")]
    [AllowAnonymous]
    public async Task<IActionResult> Login([FromForm] JwtUser applicationUser)
    {
      var identity = await GetClaimsIdentity(applicationUser);

      if (identity == null)
      {
        _logger.LogInformation($"Invalid username ({applicationUser.UserName}) or password ({applicationUser.Password})");
        return BadRequest("Invalid credentials");
      }

      var claims = new[]
      {
        new Claim(JwtRegisteredClaimNames.Sub, applicationUser.UserName),
        new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
        new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
        identity.FindFirst("DisneyCharacter")
      };

      // Create the JWT security token and encode it.
      var jwt = new JwtSecurityToken(
          issuer: _jwtOptions.Issuer,
          audience: _jwtOptions.Audience,
          claims: claims,
          notBefore: _jwtOptions.NotBefore,
          expires: _jwtOptions.Expiration.AddDays(5),
          signingCredentials: _jwtOptions.SigningCredentials);

      var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

      // Serialize and return the response
      var response = new
      {
        access_token = encodedJwt,
        expires_in = (int)_jwtOptions.ValidFor.TotalSeconds
      };

      var json = JsonConvert.SerializeObject(response, _serializerSettings);

      return new OkObjectResult(json);
    }

    /*
     // https://weblog.west-wind.com/posts/2017/May/15/Upgrading-to-NET-Core-20-Preview 
    [AllowAnonymous]
    [HttpPost]
    [Route("api/login")]
    public async Task<bool> Login([FromBody]  User loginUser)
    {
      var user = await accountRepo.AuthenticateAndLoadUser(loginUser.Username, loginUser.Password);

      if (user == null)
        throw new ApiException("Invalid Login Credentials", 401);

      var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
      identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));

      if (user.Fullname == null)
        user.Fullname = string.Empty;
      identity.AddClaim(new Claim("FullName", user.Fullname));

      await HttpContext.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
          new ClaimsPrincipal(identity));

      return true;
    }
    */
    private void ThrowIfInvalidOptions(JwtIssuerOptions options)
    {
      if (options == null) throw new ArgumentNullException(nameof(options));

      if (options.ValidFor <= TimeSpan.Zero)
      {
        throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor));
      }

      if (options.SigningCredentials == null)
      {
        throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials));
      }

      if (options.JtiGenerator == null)
      {
        throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator));
      }
    }

    /// <returns>Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC).</returns>
    private long ToUnixEpochDate(DateTime date) => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);


    private Task<ClaimsIdentity> GetClaimsIdentity(JwtUser jwtUser)
    {
      if (jwtUser.UserName == "aa" &&
          jwtUser.Password == "bb")
      {
        return Task.FromResult(new ClaimsIdentity(new GenericIdentity(jwtUser.UserName, "Token"),
          new[]
          {
            new Claim("DisneyCharacter", "IAmMickey")
          }));
      }

      if (jwtUser.UserName == "aa" &&
          jwtUser.Password == "bbb")
      {
        return Task.FromResult(new ClaimsIdentity(new GenericIdentity(jwtUser.UserName, "Token"),
          new Claim[] { }));
      }

      // Credentials are invalid, or account doesn't exist
      return Task.FromResult<ClaimsIdentity>(null);
    }
  }
}